/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.iec.edp.caf.commons.cryptography;

import io.iec.edp.caf.common.enums.SymmetricAlgorithmEnum;
import io.iec.edp.caf.commons.cryptography.providers.*;
import io.iec.edp.caf.commons.cryptography.providers.RC2CryptoProvider;
import io.iec.edp.caf.commons.cryptography.providers.SM4CryptoProvider;
import io.iec.edp.caf.commons.cryptography.service.SymmetricCryptoProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

/**
 * 加密/解密工具类
 * 修改：文档传化byte流时末尾可能会有0x00，所以传入byte[]加密时填充补位时增加标识
 *
 * @author guowenchang
 */
public class SymmetricCryptographer {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private SymmetricCryptoProvider cryptographyService;
    private SymmetricAlgorithmEnum symmetricAlgorithmEnum;

    /**
     * 私有构造参数
     *
     * @param symmetricAlgorithmEnum 加密方式
     */
    private SymmetricCryptographer(SymmetricAlgorithmEnum symmetricAlgorithmEnum) {
        this.symmetricAlgorithmEnum = symmetricAlgorithmEnum;
        switch (symmetricAlgorithmEnum) {
//            因为安全问题去除
//            case DES:
//                this.cryptographyService = new DESCryptoProvider();
//                break;
            case TripleDES:
                this.cryptographyService = new TripleDESCryptoProvider();
                break;
            case RC2:
                this.cryptographyService = new RC2CryptoProvider();
                break;
            case Rijndael://AES
                this.cryptographyService = new AESCryptoProvider();
                break;
            case SM4:
                this.cryptographyService = new SM4CryptoProvider();
                break;
            default:
                throw new RuntimeException("Unknown Cryptographer");
        }
    }

    /**
     * 公有静态构造器, 返回加密工具实例
     *
     * @param symmetricAlgorithmEnum 加密方式
     * @return 对象实例
     */
    public static SymmetricCryptographer getInstance(SymmetricAlgorithmEnum symmetricAlgorithmEnum) {
        return new SymmetricCryptographer(symmetricAlgorithmEnum);
    }

    /**
     * 最终加密
     * @param byteIn 传入byte[]
     * @param key 加密key
     * @return 密文
     */
    private byte[] finalEncrypt(byte[] byteIn, String key){
        byte[] keyByte = getLegalKey(key);
        byte[] vector = getLegalIV(key);
        return cryptographyService.encrypt(byteIn, keyByte, vector);
    }

    /**
     * 加密数据
     *
     * @param byteIn 明文
     * @param key    密码
     * @return 密文
     */
    public byte[] encrypt(byte[] byteIn, String key) {
        byteIn = appendBottom0WithSymbol(byteIn, this.symmetricAlgorithmEnum.getBlockSize());
        return finalEncrypt(byteIn,key);
    }

    /**
     * 解密数据
     *
     * @param byteIn 密文
     * @param key    密码
     * @return 明文
     */
    public byte[] decrypt(byte[] byteIn, String key) {
        byte[] keyByte = getLegalKey(key);
        byte[] vector = getLegalIV(key);
        return removeBottomZero(cryptographyService.decrypt(byteIn, keyByte, vector));
    }

    /**
     * 加密数据
     *
     * @param stringIn 明文
     * @param key      密码
     * @return 密文
     */
    public String encrypt(String stringIn, String key) {
        byte[] byteIn = stringIn.getBytes(StandardCharsets.UTF_8);
        byteIn = appendBottomZero(byteIn,this.symmetricAlgorithmEnum.getBlockSize());
        //byte[] byteOut = this.encrypt(byteIn, key);
        byte[] byteOut = this.finalEncrypt(byteIn, key);
        return Base64.getEncoder().encodeToString(byteOut);
    }

    /**
     * 解密数据
     *
     * @param stringIn 密文
     * @param key      密码
     * @return 明文
     */
    public String decrypt(String stringIn, String key) {
        byte[] byteIn = Base64.getDecoder().decode(stringIn);
        byte[] byteOut = this.decrypt(byteIn, key);
        return new String(byteOut, StandardCharsets.UTF_8);
    }

    /**
     * 由于不同加密算法对key的长度有着不同要求, 因此需要调用本方法
     * 对key做预处理, 使其满足加密算法的长度要求
     *
     * @param key 初始密钥
     * @return 合法密钥
     */
    private byte[] getLegalKey(String key) {
        String resultString;
        if (this.symmetricAlgorithmEnum.getNeedLegalKey()) {
            int minSize = this.symmetricAlgorithmEnum.getMinSize();
            int maxSize = this.symmetricAlgorithmEnum.getMaxSize();
            int skipSize = this.symmetricAlgorithmEnum.getSkipSize();
            int length = key.length() * 8;

            if (length <= minSize) {
                //小于等于最小长度 右侧补齐空格
                resultString = String.format("%-" + minSize / 8 + "s", key);
            } else if (length <= maxSize) {
                //大于最小长度 且小于等于最大长度 计算出位置后填充字符串
                length = ((length - 1) / skipSize + 1) * skipSize;
                resultString = String.format("%-" + length / 8 + "s", key);
            } else {
                //大于最大长度 直接截取字符串
                resultString = key.substring(0, maxSize / 8);
            }
        } else {
            resultString = key;
        }

        return resultString.getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 获取初始向量
     * 初始向量长度需与分组大小一致
     *
     * @param key
     * @return 初始向量
     */
    private byte[] getLegalIV(String key) {
        int blockSize = this.symmetricAlgorithmEnum.getBlockSize();
        if (key.length() > blockSize / 8) {
            key = key.substring(0, blockSize / 8);
        }

        return String.format("%-" + blockSize / 8 + "s", key).getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 先填充0x01，保证之后去除时不会将本身末尾为0的值去除
     * 在byte数组后填充0x00 使其可以被分组大小整除
     *
     * @param bytes     明文
     * @param blockSize 分组大小
     * @return
     */
    private byte[] appendBottom0WithSymbol(byte[] bytes, int blockSize){
        int length = bytes.length;
        blockSize = blockSize / 8;

        //计算需填充长度
        if (length % blockSize != 0) {
            length = length + (blockSize - (length % blockSize));
        }else{
            //如果刚好够的话也要填充1，所以要再扩展一个blocksize
            length = length + blockSize;
        }

        //填充
        byte[] result = new byte[length];
        System.arraycopy(bytes, 0, result, 0, bytes.length);

        //填充1
        result[bytes.length] = 1;
        return result;
    }

    /**
     * 在byte数组后填充0x00 使其可以被分组大小整除
     *
     * @param bytes     明文
     * @param blockSize 分组大小
     * @return
     */
    private byte[] appendBottomZero(byte[] bytes, int blockSize) {
        int length = bytes.length;
        blockSize = blockSize / 8;

        //计算需填充长度
        if (length % blockSize != 0) {
            length = length + (blockSize - (length % blockSize));
        }

        //填充
        byte[] result = new byte[length];
        System.arraycopy(bytes, 0, result, 0, bytes.length);

        return result;
    }

    /**
     * 移除末尾的0x00，和之前填充的0x01
     *
     * @param bytes
     * @return
     */
    private byte[] removeBottom0WithSymbol(byte[] bytes) {
        int endPosition = bytes.length;
        for (int i = bytes.length; i > 0; i--) {
            endPosition = i;
            if (bytes[i - 1] != 0x00) {
                break;
            }
        }
        //把1扣掉
        if(bytes[endPosition-1]==1) endPosition--;
        byte[] result = new byte[endPosition];
        System.arraycopy(bytes, 0, result, 0, endPosition);

        return result;
    }

    /**
     * 移除末尾的0x00，和之前填充的0x01
     *
     * @param bytes
     * @return
     */
    private byte[] removeBottomZero(byte[] bytes) {
        int endPosition = bytes.length;
        for (int i = bytes.length; i > 0; i--) {
            endPosition = i;
            if (bytes[i - 1] != 0x00) {
                break;
            }
        }
        //把1扣掉 加的判断逻辑是为了兼容老的加密问题解密时正确
        //如果区别开用removeBottom0WithSymbol 可以去掉
        if(bytes[endPosition-1]==1) endPosition--;

        byte[] result = new byte[endPosition];
        System.arraycopy(bytes, 0, result, 0, endPosition);

        return result;
    }
}
